跳到主要内容

gRPC 异常处理

下文转自 gRPC 扩展错误处理

HTTP 协议错误处理基本会通过 status code 和请求响应(自定义消息)来传递错误,而 gRPC 这边错误处理就没有 HTTP 这么好控制。

gRPC 默认使用 Status 来表示错误, 这个结构包含了 code 和 message 两个字段。 code 是类似于 http status code 的一系列错误类型的 枚举, 所有语言 sdk 都会内置这个枚举列表, 而 message 就是服务端需要告知客户端的一些错误详情信息。

以 Golang 为例:

// 发送错误响应
err := status.Errorf(codes.InvalidArgument, "invalid args")

// 错误转回 status
// 转换有可能失败
st, ok := status.FromError(err)
fmt.Println(st.Code(), st.Message())

有人建了一个仓库展示了几乎所有语言的 gRPC 错误处理方式: https://avi.im/grpc-errors

默认错误处理方式非常简单直白, 但是有个很大的问题就是表达能力非常有限。 因为使用类似于 HTTP 状态码的有限抽象 code 没法表达出多样的业务层的错误,而 message 这种字符串也是不应该被请求方当做业务错误标识符来使用。所以我们需要一个额外能够传递业务错误码甚至更多额外错误信息字段的功能。

Richer error model

这里使用 Google 的错误模型 对这种进行处理。它自己定义了一个 protobuf 错误消息类constraint

package google.rpc;

// The `Status` type defines a logical error model that is suitable for
// different programming environments, including REST APIs and RPC APIs.
message Status {
// A simple error code that can be easily handled by the client. The
// actual error code is defined by `google.rpc.Code`.
int32 code = 1;

// A developer-facing human-readable error message in English. It should
// both explain the error and offer an actionable resolution to it.
string message = 2;

// Additional error information that the client code can use to handle
// the error, such as retry info or a help link.
repeated google.protobuf.Any details = 3;
}

可以看到比标准错误多了一个 details 数组字段, 而且这个字段是 Any 类型, 支持我们自行扩展.

那么问题来了, 如何传递这个非标准的错误扩展消息呢? 答案是放在 trailing response metadata 中, key 为 grpc-status-details-bin.

这个功能只被部分语言 sdk 支持了, 所以有些不被支持的语言想要使用这个功能需要手动处理.

由于 Golang 支持了这个扩展, 所以可以看到 Status 直接就是有 details 字段的.

// 使用 WithDetails 附加自己扩展的错误类型, 该方法会自动将我们的扩展类型转换为 Any 类型
st, err := status.New(codes.Unknown, "test error").WithDetails(&pb.BizError{})
// 将 st.Err() 当做 error 返回
if err == nil {
return st.Err()
}

st, ok := status.FromError(err)
if ok {
// 直接可以读取 details
fmt.Printf("%+v\n", st.Details())
}

grpc-go 源码搜索 grpc-status-details-bin 可以看到相关源码:

// 发送错误
// https://github.com/grpc/grpc-go/blob/23a83dd097ec07fc7ddfb4a30c675763e4972ba4/internal/transport/handler_server.go#L205
func (ht *serverHandlerTransport) WriteStatus(s *Stream, st *status.Status) error {
// ...
// 包含 details 时, 将 status 消息序列化放到 metadata 中
if p := st.Proto(); p != nil && len(p.Details) > 0 {
stBytes, err := proto.Marshal(p)
if err != nil {
// TODO: return error instead, when callers are able to handle it.
panic(err)
}

h.Set("Grpc-Status-Details-Bin", encodeBinHeader(stBytes))
}
// ...
}

// 接收错误
// https://github.com/grpc/grpc-go/blob/40916aa021698425b1685741a48315a4c675bc92/internal/transport/http2_client.go#L1343
func (t *http2Client) operateHeaders(frame *http2.MetaHeadersFrame) {
// ...
case "grpc-status-details-bin":
var err error
statusGen, err = decodeGRPCStatusDetails(hf.Value)
if err != nil {
headerError = fmt.Sprintf("transport: malformed grpc-status-details-bin: %v", err)
}
// ...
}

值得一提的是, Golang 提供的 status.Details() 方法已经将 details 中的 Any 消息进行了动态反序列化, 也就是只要是你 protobuf 包含的类型, 直接可以使用 detail.(*Type) 来进行转换, 但是如果出现未知类型你将会得到一个 error

Example

这里使用官方示例来介绍如何响应错误

服务端

// server.go 
package main

import (
"fmt"
"log"
"net"

"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

api "github.com/avinassh/grpc-errors/go/hello"
)

type HelloServer struct {
api.UnimplementedHelloServiceServer
}

func (s *HelloServer) SayHello(ctx context.Context, req *api.HelloReq) (*api.HelloResp, error) {
return &api.HelloResp{Result: fmt.Sprintf("Hey, %s!", req.GetName())}, nil
}

func (s *HelloServer) SayHelloStrict(ctx context.Context, req *api.HelloReq) (*api.HelloResp, error) {
if len(req.GetName()) >= 10 {
return nil, status.Errorf(codes.InvalidArgument,constraint

func Serve() {
addr := fmt.Sprintf(":%d", 50051)
conn, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("Cannot listen to address %s", addr)
}

s := grpc.NewServer()
api.RegisterHelloServiceServer(s, &HelloServer{})
if err := s.Serve(conn); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
constraint

客户端

// client.go
package main

import (
"fmt"
"log"

"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

api "github.com/avinassh/grpc-errors/go/hello"constraint
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("Did not connect: %v", err)
}
defer conn.Close()
c := api.NewHelloServiceClient(conn)

// ideally, you should handle error here too, for brevity
// I am ignoring that
resp, _ := c.SayHello(
context.Background(),
&api.HelloReq{Name: "Euler"},
)
fmt.Println(resp.GetResult())

resp, err = c.SayHelloStrict(
context.Background(),
&api.HelloReq{Name: "Leonhard Euler"},
)

if err != nil {
// ouch!
// lets print the gRPC error message
// which is "Length of `Name` cannot be more than 10 characters"
errStatus, _ := status.FromError(err)
fmt.Println(errStatus.Message())
// lets print the error code which is `INVALID_ARGUMENT`
fmt.Println(errStatus.Code())
// Want its int version for some reason?
// you shouldn't actullay do this, but if you need for debugging,
// you can do `int(status_code)` which will give you `3`
//
// Want to take specific action based on specific error?
if codes.InvalidArgument == errStatus.Code() {
// do your stuff here
log.Fatal()
}
}

fmt.Println(resp.GetResult())
}

References